<?php 
/**
 * @author Erling Owe <erling.owe@gmail.com>
 * @package Core
 * @subpackage IO
 */ 
   
class Core_IO_HTTPRequest { 
   
   const METHOD_POST = 1;
   const METHOD_GET = 2;
   
   private $strURL;
   private $intPort;
   
   private $constMethod;
   
   private $arrHeaders = array();
   private $strContent;

   private $strResponse = "";
   
   function __construct ( $strURL ) {
      
      $this->strURL = $strURL;
      
      $this->arrParseURL = parse_url( $strURL );
      
   }

   /**
    * Set the port to use for the request.
    *
    * This will overwrite any what's set in the URL. If not set at all, it defaults to port 80.
    *
    * @param integer $intPort
    */
   public function setPort( $intPort ) {
      $this->intPort = $intPort;
   }
   
   public function setMethod ( $constMethod ) {
      $this->constMethod = $constMethod;
   }

   /**
    * Add a HTTP header.
    *
    * @param string $strHeader
    */
   public function addHeader( $strHeader ) {
      $this->arrHeaders[] = $strHeader;   
   }

   /**
    * @access private
    */
   public function setContent ( $strContent ) {
      $this->strContent = $strContent;
   }

   /**
    * Execute the request.
    *
    * @exception Could not connect to host. <i>Exception code 200</i>
    * @todo Overwrite headers that are the same instead of appending them and end up with duplicates.
    * @todo Fix bug occouring when using GET method.
    * @todo Make it possible to set a timeout in seconds and throw an exception if met.
    */
   public function exec () {     

      $arrParseURL = parse_url( $this->strURL );

      // Ignore the port from the URL if it's been set by setPort(), and default to 80 if it's not set at all.
      $intPort = ( $this->intPort ) ? $this->intPort : ( isset( $arrParseURL['port'] ) ) ? $arrParseURL['port'] : 80;

      $resConnection = fsockopen( $arrParseURL['host'], $intPort );
        
      if ( $resConnection ) {

         $strHeaders = "";
         
         if ( $this->constMethod == self::METHOD_POST ) {
            $strHeaders .= "POST " . $arrParseURL['path'] . " HTTP/1.1\r\n";
         } else {
            $strHeaders .= "GET " . $arrParseURL['path'] . "?" . $arrParseURL['query'] . " HTTP/1.0\r\n";
         }
            
         $strHeaders .= "Host: " . $arrParseURL['host'] . "\r\n";
         //$strHeaders .= "Accept: text/*\r\n";
         $strHeaders .= "Connection: Close\r\n";
            
         // HTTP Authorization.
         if ( isset( $arrParseURL['user'] ) and isset( $arrParseURL['pass'] ) ) {
            $strHeaders .= "Authorization: Basic " . base64_encode( $arrParseURL['user'] . ":" . $arrParseURL['pass'] ) . "\r\n";
         }
         
         // Appending added headers.
         foreach ( $this->arrHeaders as $strHeader ) {
            $strHeaders .= $strHeader . "\r\n";            
         }

         if ( $this->constMethod == self::METHOD_POST ) {
            $strHeaders .= "Content-Type: application/x-www-form-urlencoded\r\n";
            $strHeaders .= 'Content-Length: ' . strlen( $this->strContent ) . "\r\n";
            $strRequest = $strHeaders . "\r\n" . $this->strContent;
         } else {
            $strRequest = $strHeaders;
         }
            

         fwrite( $resConnection, $strRequest );
         while ( !feof( $resConnection ) ) {
             $this->strResponse .= fgets( $resConnection );
         }
      
         fclose( $resConnection );

         return new Core_IO_HTTPRequest_Response( $this->strResponse );
         
      } else {
      
         throw new Exception ( "Could not connect to '" . $this->getHost() . "'", 200 );
         
      }

   }

   /**
    * Returns the raw response.
    *
    * @return string
    */
   public function getResponse () {
      return $this->strResponse;
   }
   
}

class Core_IO_HTTPRequest_Response {

   private $strResponse;

   private $arrStatus;
   private $arrHeaders;
   private $strContent;

   function __construct ( $strResponse ) {

      $this->strResponse = $strResponse;

      $arr = explode( "\r\n", $strResponse, 2 );
      list( , $this->arrStatus['StatusCode'], $this->arrStatus['ReasonPhrase'] ) = explode( " ", $arr[0] );

      $arr = explode( "\r\n\r\n", $arr[1], 2 );

      foreach ( explode( "\r\n", $arr[0] ) as $strHeader ) {
         list( $strName, $strValue ) = explode( ":", $strHeader );
         $this->arrHeaders[$strName] = trim( $strValue );
      }

      if ( $this->getHeader( "Transfer-Encoding" ) == "chunked" ) {

         // ToDo: Write this so that it can read multiple chunks and additional headers.

         $arr = explode( "\r\n", $arr[1], 2 );
         $intChunkLength = hexdec($arr[0]);

         $this->strContent = substr( $arr[1], 0, $intChunkLength );

      } else {
         $this->strContent = $arr[1];
      }      

   }

   /**
    * Get the status of the request.
    *
    * @return array An associative array consisting of the 'StatusCode' and 'ResponsePhrase'.
    */
   public function getStatus () {
      return $this->arrStatus;
   }

   /**
    * Gets a header from the response.
    * 
    * @param string $strName
    * @return boolean|string The value as a string, <kbd>false</kbd> if the header wasn't in the response.
    */
   public function getHeader ( $strName ) {
      return ( isset( $this->arrHeaders[$strName] ) ) ? $this->arrHeaders[$strName] : false;
   }

   public function getContent () {
      return $this->strContent;
   }

}

?>